package org.dflish.mybatis.encrypt.core;

import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.util.StrUtil;
import lombok.Data;
import net.sf.jsqlparser.JSQLParserException;
import net.sf.jsqlparser.expression.Expression;
import net.sf.jsqlparser.expression.JdbcParameter;
import net.sf.jsqlparser.expression.operators.relational.EqualsTo;
import net.sf.jsqlparser.parser.CCJSqlParserUtil;
import net.sf.jsqlparser.schema.Column;
import net.sf.jsqlparser.statement.Statement;
import net.sf.jsqlparser.statement.insert.Insert;
import net.sf.jsqlparser.statement.select.PlainSelect;
import net.sf.jsqlparser.statement.select.Select;
import net.sf.jsqlparser.statement.update.Update;
import net.sf.jsqlparser.util.deparser.ExpressionDeParser;
import org.dflish.mybatis.encrypt.rewrite.condition.EncryptCondition;
import org.dflish.mybatis.encrypt.rule.EncryptRule;
import org.dflish.mybatis.encrypt.util.TablesNamesFinderPlus;

import java.util.*;

/**
 * @author liangwx
 */
@Data
public class CCJSQLStatementContext {

    private Statement statements;

    private List<CCJSQLTableDTO> tableList;

    private final EncryptRule encryptRule;

    private String sql;

    public CCJSQLStatementContext(String sql, EncryptRule encryptRule) throws JSQLParserException {

        this.sql = sql;
        this.encryptRule = encryptRule;
        Statement statements = CCJSqlParserUtil.parse(sql);
        this.statements = statements;

        TablesNamesFinderPlus tablesNamesFinderPlus = new TablesNamesFinderPlus();
        tableList = tablesNamesFinderPlus.getTableList(statements);
    }

    private List<CCJSQLCryptExpressionDTO> parseInsert(Insert insert){
        List<Column> columns = insert.getColumns();
        List<CCJSQLCryptExpressionDTO> result = new ArrayList<>();
        for (int i = 0; i < columns.size(); i++) {
            Column column = columns.get(i);
            CCJSQLCryptExpressionDTO dto = new CCJSQLCryptExpressionDTO();
            dto.setAlias(TablesNamesFinderPlus.getAlias(column.getTable()));
            dto.setColumnName(column.getColumnName());
            dto.setIndex(i + 1);
            result.add(dto);
        }
        return result;
    }

    private List<CCJSQLCryptExpressionDTO> parseUpdate(Update update){
        List<Column> columns = update.getColumns();
        List<CCJSQLCryptExpressionDTO> result = new ArrayList<>();
        for (int i = 0; i < columns.size(); i++) {
            Column column = columns.get(i);
            CCJSQLCryptExpressionDTO dto = new CCJSQLCryptExpressionDTO();
            dto.setAlias(TablesNamesFinderPlus.getAlias(column.getTable()));
            dto.setColumnName(column.getColumnName());
            dto.setIndex(i + 1);
            result.add(dto);
        }
        return result;
    }

    public List<EncryptCondition> createEncryptConditions() {

        List<CCJSQLCryptExpressionDTO> list = null;

        if (statements instanceof Select){
            Select select = (Select) statements;
            list = parseSelect(select);
        } else if (statements instanceof Insert){
            Insert insert = (Insert) statements;
            list = parseInsert(insert);
        } else if (statements instanceof Update) {
            Update update = (Update) statements;
            list = parseUpdate(update);
        }

        if (CollectionUtil.isEmpty(list)){
            return Collections.emptyList();
        }

        List<EncryptCondition> result = new LinkedList<>();
        for (CCJSQLCryptExpressionDTO dto : list){
            result.addAll(createEncryptConditions(dto));
        }

        return result;
    }



    public List<CCJSQLCryptExpressionDTO> parseSelect(Select select) {

        StringBuilder b = new StringBuilder();

        List<CCJSQLCryptExpressionDTO> list = new ArrayList<>();

        PlainSelect plain = (PlainSelect) select.getSelectBody();
        Expression where = plain.getWhere();

        where.accept(new ExpressionDeParser(null, b) {
            @Override
            public void visit(EqualsTo equalsTo) {
                Column leftExpression = (Column) equalsTo.getLeftExpression();

                Expression rightExpression = equalsTo.getRightExpression();

                if (rightExpression instanceof JdbcParameter){
                    JdbcParameter jdbcParameter = (JdbcParameter) rightExpression;
                    Integer index = jdbcParameter.getIndex();
                    CCJSQLCryptExpressionDTO dto = new CCJSQLCryptExpressionDTO();
                    dto.setAlias(TablesNamesFinderPlus.getAlias(leftExpression.getTable()));
                    dto.setColumnName(leftExpression.getColumnName());
                    dto.setIndex(index);
                    list.add(dto);
                }
            }
        });

        return list;
    }

    private Collection<EncryptCondition> createEncryptConditions(CCJSQLCryptExpressionDTO dto) {
        Collection<EncryptCondition> result = new LinkedList<>();
        createEncryptCondition(dto).ifPresent(result::add);
        return result;
    }

    private Optional<EncryptCondition> createEncryptCondition(CCJSQLCryptExpressionDTO dto){
        Optional<String> tableName = findTableName(dto);
        final EncryptCondition condition = new EncryptCondition(tableName.get(), dto.getColumnName(), dto.getIndex());
        return tableName.isPresent() && containsColumn(tableName.get(), dto.getColumnName())
                ? Optional.of(condition) : Optional.empty();
    }

    public Optional<String> findTableName(CCJSQLCryptExpressionDTO dto){

        if (tableList.size() == 1){
            return Optional.of(CollectionUtil.getFirst(tableList).getTableName());
        } else if (StrUtil.isNotEmpty(dto.getAlias())){
            return Optional.of(findTableNameFromSQL(dto.getAlias()));
        }

        return findTableNameFromMetaData(dto.getColumnName());
    }

    private Optional<String> findTableNameFromMetaData(final String columnName) {
        for (CCJSQLTableDTO each : tableList) {
            if (containsColumn(each.getTableName(), columnName)) {
                return Optional.of(each.getTableName());
            }
        }
        return Optional.empty();
    }

    private boolean containsColumn(String tableName, String columnName){
        return encryptRule.containsColumn(tableName, columnName);
    }

    private String findTableNameFromSQL(final String tableNameOrAlias) {
        for (CCJSQLTableDTO each : tableList) {
            if (tableNameOrAlias.equalsIgnoreCase(each.getTableName()) || tableNameOrAlias.equals(each.getAlisaName())) {
                return each.getTableName();
            }
        }
        throw new IllegalStateException("Can not find owner from table.");
    }
}
